using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Diagnostics;

namespace Nox.Amuse.Model
{
	/// <summary>
	/// Enums the possible states for text to be formatted by ANSI codes in
	/// </summary>
	[Flags]
	public enum FormattingProperties
	{
		None = 0,
		Bold = 1,
		Flash = 2,
		Underline = 4,
		Invert = 8
	}

	/// <summary>
	/// Enums the possible colours with values as their Foreground numeric codes, add 10 for background codes
	/// </summary>
	public enum FormattingColor
	{
		Unset = 0,
		Black = 30,
		Red = 31,
		Green = 32,
		Yellow = 33,
		Blue = 34,
		Magenta = 35,
		Cyan = 36,
		White = 37
	}

	/// <summary>
	/// Defines an object that stores a UI neutral view of a run of text with a specific set of server applied formatting
	/// </summary>
	public struct LineItemRun
	{
		/// <summary>
		/// The displayable text
		/// </summary>
		public string RunText { get; set; }
		/// <summary>
		/// True if this text run resets formatting to default
		/// </summary>
		public bool HasResetCode { get; set; }
		/// <summary>
		/// The formatting to be applied
		/// </summary>
		public FormattingProperties Formatting { get; set; }
		/// <summary>
		/// The foreground colour for the text
		/// </summary>
		public FormattingColor Foreground { get; set; }
		/// <summary>
		/// The background colour for the text
		/// </summary>
		public FormattingColor Background { get; set; }

		/// <summary>
		/// Returns the hash code for this LineItemRun
		/// </summary>
		/// <returns>The hash code</returns>
		public override int GetHashCode()
		{
			return RunText.GetHashCode() ^ HasResetCode.GetHashCode() ^ Formatting.GetHashCode() ^ Foreground.GetHashCode() ^ Background.GetHashCode();
		}

		/// <summary>
		/// Compares this LineItemRun against the given object to see if they are equal
		/// </summary>
		/// <param name="obj">The object to be tested</param>
		/// <returns>True if the object and this LineItemRun are equal</returns>
		public override bool Equals(object obj)
		{
			if (!(obj is LineItemRun))
				return false;

			return Equals((LineItemRun)obj);
		}

		/// <summary>
		/// Compared this LineItemRun against the given LineItemRun to see if they are equal
		/// </summary>
		/// <param name="other">The other LineItemRun to be tested against</param>
		/// <returns>True if the other LineItemRun and this one are equal</returns>
		public bool Equals(LineItemRun other)
		{
			if (RunText == other.RunText
				&& HasResetCode == other.HasResetCode
				&& Formatting == other.Formatting
				&& Foreground == other.Foreground
				&& Background == other.Background)
				return true;
			else
				return false;
		}

		/// <summary>
		/// Impliments logic for the equality operator
		/// </summary>
		/// <param name="run1">The first LineItemRun to be compared</param>
		/// <param name="run2">The second LineItemRun to be compared</param>
		/// <returns>True if the two LineItemRuns are equal</returns>
		public static bool operator ==(LineItemRun run1, LineItemRun run2)
		{
			return run1.Equals(run2);
		}

		/// <summary>
		/// Impliments the logic for the inequality operator
		/// </summary>
		/// <param name="run1">The first LineItemRun to be compared</param>
		/// <param name="run2">The second LineItemRun to be compared</param>
		/// <returns>True of the two LineItemRuns are not equal</returns>
		public static bool operator !=(LineItemRun run1, LineItemRun run2)
		{
			return !run1.Equals(run2);
		}

	}

	/// <summary>
	/// Represents a single line of text
	/// </summary>
	public class LineItem
	{
		//Definitions
		/// <summary>
		/// Stores a reference to the command code for Bold
		/// </summary>
		const string Bold = "1";
		/// <summary>
		/// Stores a reference to the command code for Flash
		/// </summary>
		const string Flash = "5";
		/// <summary>
		/// Stores a reference to the command code for Underline
		/// </summary>
		const string Underline = "4";
		/// <summary>
		/// Stores a reference to the command code for Invert
		/// </summary>
		const string Invert = "7";

		//MetaData
		/// <summary>
		/// Gets or sets the Guid for this LineItem
		/// </summary>
		public Guid Guid { get; private set; }
		/// <summary>
		/// Gets or sets the Previous LineItem to arrive before this one
		/// </summary>
		public LineItem PreviousLine { get; private set; }

		//ActualData
		/// <summary>
		/// Gets or sets the original text as it was received from the world
		/// </summary>
		public string RawString { get; private set; }
		/// <summary>
		/// Gets or sets the date and time the line arrived
		/// </summary>
		public DateTime ArrivalTime { get; private set; }

		//Constructor
		/// <summary>
		/// Public constructor for the LineItem
		/// </summary>
		/// <param name="raw">The original raw string received from the server</param>
		/// <param name="previousLine">The LineItem prior to this one to arrive</param>
		public LineItem(string raw, LineItem previousLine)
		{
			this.ArrivalTime = DateTime.Now;
			this.Guid = Guid.NewGuid();
			this.PreviousLine = previousLine;
			this.RawString = raw;
		}

		/// <summary>
		/// Stores the formatting state of the end of this line, based on combinded formatting codes for all the LineItemRuns
		/// </summary>
		private LineItemRun EndStateCache = new LineItemRun();
		/// <summary>
		/// Gets the formatting state of the end of this line, based on combinded formatting codes for all the LineItemRuns
		/// </summary>
		public LineItemRun EndState
		{
			get
			{
				if (EndStateCache == null)
				{
					GetFormattedLineRuns();
				}
				return EndStateCache;
			}
			set
			{
				EndStateCache = value;
			}
		}

		//Regular Expressions
		/// <summary>
		/// Matches 'escape', and maybe '[' and maybe a digit and maybe ';' any number of times, maybe followed by 'm' or 'z'
		/// </summary>
		private static Regex regexAnyTag = new Regex(@"(\e(\[([\d;]*[mz]?))?)?", RegexOptions.Compiled);
		/// <summary>
		/// Matches 'escape'
		/// </summary>
		private static Regex regexEscape = new Regex(@"\e", RegexOptions.Compiled);
		/// <summary>
		/// Matches one or two digits that in front of '[' or ';', and behind ';' or 'm'
		/// </summary>
		private static Regex rDigit = new Regex(@"((?<=\[)|(?<=;))\d{1,2}((?=;)|(?=m))", RegexOptions.Compiled);

		//Parsing Methods
		/// <summary>
		/// Removes all formmatting codes from the raw and returns a plain one
		/// </summary>
		/// <returns>A new plain string without any codes</returns>
		public string GetPlainString()
		{
			return regexAnyTag.Replace(RawString, string.Empty);
		}
		/// <summary>
		/// Create a List of LineItemRun structs by parsing a raw string with formatting codes
		/// </summary>
		/// <remarks>Running this method also sets the EndState property for this LineItem</remarks>
		/// <returns>A generic list of LineItemRun's, each containing a portion of the line and the codes to format it</returns>
		public List<LineItemRun> GetFormattedLineRuns()
		{
			if (regexEscape.IsMatch(RawString))
			{
				var subStrings = new List<string>(regexEscape.Split(RawString));

				if (String.IsNullOrEmpty(subStrings[0]))
					subStrings.RemoveAt(0); //Clears first item if its empty

				LineItemRun endStatePrep = new LineItemRun();
				if (PreviousLine != null)
					endStatePrep = this.PreviousLine.EndState; //Copy EndState

				var formattedRuns = new List<LineItemRun>(subStrings.Count);

				foreach (var subString in subStrings)
				{
					var run = new LineItemRun();

					if ((subString[0] == '[') && (subString.Length > 1))
					{
						var match = rDigit.Match(subString);
						if (match.Success)
						{
							do
							{
								switch (match.Value)
								{
									case "0":
										run.HasResetCode = true;
										endStatePrep.HasResetCode = true;
										endStatePrep.Formatting = FormattingProperties.None;
										endStatePrep.Foreground = FormattingColor.Unset;
										endStatePrep.Background = FormattingColor.Unset;
										break;
									case Bold:
										run.Formatting = run.Formatting | FormattingProperties.Bold;
										endStatePrep.Formatting = endStatePrep.Formatting | FormattingProperties.Bold;
										endStatePrep.HasResetCode = false;
										break;
									case Flash:
										run.Formatting = run.Formatting | FormattingProperties.Flash;
										endStatePrep.Formatting = endStatePrep.Formatting | FormattingProperties.Flash;
										endStatePrep.HasResetCode = false;
										break;
									case Underline:
										run.Formatting = run.Formatting | FormattingProperties.Underline;
										endStatePrep.Formatting = endStatePrep.Formatting | FormattingProperties.Underline;
										endStatePrep.HasResetCode = false;
										break;
									case Invert:
										run.Formatting = run.Formatting | FormattingProperties.Invert;
										endStatePrep.Formatting = endStatePrep.Formatting | FormattingProperties.Invert;
										endStatePrep.HasResetCode = false;
										break;
									default:
										//The number might be colour
										int value = int.Parse(match.Value, CultureInfo.InvariantCulture);
										if (value >= 30 && value <= 37)
										{
											run.Foreground = (FormattingColor)value;
											endStatePrep.Foreground = (FormattingColor)value;
										}
										else if (value >= 40 && value <= 47)
										{
											run.Background = (FormattingColor)(value - 10);
											endStatePrep.Background = (FormattingColor)(value - 10);
										}
										break;
								}

								// Following protects against possible Index Out Of Bounds Exception
                                // Submitted by PatashuCreamtheRabbit
                                try
                                {
                                    if (subString.Length > match.Index + match.Length
                                        && subString[match.Index + match.Length] == 'm')
                                    {
                                        run.RunText = subString.Substring(match.Index + match.Length + 1);
                                        break;
                                    }
                                    else
                                    {
                                        match = match.NextMatch();
                                    }
                                }
                                catch (IndexOutOfRangeException iFault)
                                {
                                    Debug.WriteLine("IndexOutOfRangeException in m matching code. Message: " +
                                        iFault.Message);
                                    throw;
                                }
							} while (match.Success);
						}
						else
						{
							//This code is required for a bug in some versions of PennMUSH where an ANSI tag is issued with no number code
							if (subString[1] == 'm')
							{
								var temp = subString.Remove(0, 2);
								if (temp.Length > 0)
									run.RunText = temp;
								else
									break;
							}
							else
							{
								run.RunText = subString;
							}
						}
					}

					else { run.RunText = subString; }

					formattedRuns.Add(run);
				}
				EndState = endStatePrep;
				return formattedRuns;
			}
			else
			{
				var noFormatting = new List<LineItemRun>(1);
				noFormatting.Add(new LineItemRun { RunText = RawString });
				if (PreviousLine != null)
					EndState = PreviousLine.EndState; //Copy the previous End state because we haven't changed it
				return noFormatting;
			}
		}

	}
}
